﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Windows.Media.Imaging;
using System.Xml;
using System.IO;
using System.Windows;
using System.ComponentModel;

namespace VRacer
{
    /// <summary>
    /// Zawiera kompletna definicje trasy. Jej maske, tlo, wymiary...
    /// Implementuje takze metody IsPixelFree, IsPixelFinish, IsPixelStart ktore zwracaja odpowiednio: czy jest to sciana, czy jest to meta, czy jest to start.
    /// Sa tu takze metody zwracajace miejsca charakterystyczne na drodze ruchu. Np. pkt kolizji ze sciana GetCollisionPoint czy pkt przekroczenia mety GetFinishCrossPoint.
    /// </summary>
    [XmlRoot("Track")]
    public class Track : ICloneable, INotifyPropertyChanged
    {
        private BitmapSource _mask;
        private BitmapSource _background;
        private WriteableBitmap _bitmap;
        private Highscores _highscores;

        /// <summary>
        /// Zwraca maske trasy. Czyli bitmape ktora definiuje gdzie jest trasa a gdzie sciana i meta.
        /// </summary>
        /// <value>The mask.</value>
        [XmlIgnore]
        public BitmapSource Mask
        {
            get
            {
                if (_mask == null)
                    _mask = new BitmapImage(new Uri(Environment.CurrentDirectory + "/" + ContainingDirectory + MaskFile));


                return _mask;
            }
        }

        /// <summary>
        /// Zwraca grafike mapy. Czyli tlo ktore jest wyswietlane zamiast maski zeby ladnie wygladalo.
        /// </summary>
        /// <value>The background.</value>
        [XmlIgnore]
        public BitmapSource Background
        {
            get
            {


                if (_background == null)
                    _background = new BitmapImage(new Uri(Environment.CurrentDirectory + "/" + ContainingDirectory + BackgroundFile));

                return _background;
            }
        }

        public WriteableBitmap Bitmap
        {
            get
            {
                if (_bitmap == null)
                    _bitmap = new WriteableBitmap(Mask);
                return _bitmap;
            }
        }

        /// <summary>
        /// Preferowany wymiar mapy.
        /// </summary>
        /// <value>Dlugosc w ilosci kratek.</value>
        [XmlAttribute("Width")]
        public int Width
        {
            get;
            set;
        }

        /// <summary>
        /// Preferowany wymiar mapy.
        /// </summary>
        /// <value>Wysykosc w ilosci kratek.</value>
        [XmlAttribute("Height")]
        public int Height
        {
            get;
            set;
        }

        /// <summary>
        /// Zwraca najlepsze wyniki dla danej trasy
        /// </summary>
        /// <value>The highscores.</value>
        [XmlIgnore]
        public Highscores Highscores
        {
            get
            {
                if (_highscores == null)
                    _highscores = Highscores.Create(HighscoresFilePath);

                return _highscores;
            }
        }

        /// <summary>
        /// Zwraca sciezke pliku z najlepszymi wynikami
        /// </summary>
        /// <value>The highscores file path.</value>
        protected string HighscoresFilePath
        {
            get { return ContainingDirectory + "scores.xml"; }
        }

        /// <summary>
        /// Zwraca sciezke katalogu zawierajacego ta trase
        /// </summary>
        /// <value>The containing directory.</value>
        [XmlIgnore]
        public string ContainingDirectory
        {
            get;
            set;
        }

        /// <summary>
        /// Zwraca nazwe mapy
        /// </summary>
        /// <value>The title.</value>
        [XmlElement("Title")]
        public string Title
        {
            get;
            set;
        }

        private string _backgroundFile;
        /// <summary>
        /// Zwraca nazwe pliku zawierajacego plik tla
        /// </summary>
        /// <value>The background file.</value>
        [XmlElement("BackgroundFile")]
        public string BackgroundFile
        {
            get { return _backgroundFile; }
            set
            {
                _backgroundFile = value;
                _background = null;
                OnPropertyChanged("BackgroundFile");
                OnPropertyChanged("Background");
            }
        }

        private string _maskFile;
        /// <summary>
        /// Zwraca nazwe pliku zawierajacego plik maski
        /// </summary>
        /// <value>The mask file.</value>
        [XmlElement("MaskFile")]
        public string MaskFile
        {
            get { return _maskFile; }
            set
            {
                _maskFile = value;
                _bitmap = null;
                OnPropertyChanged("MaskFile");
                OnPropertyChanged("Mask");
            }
        }

        /// <summary>
        /// Unikalny ID mapy. Uzywany np przy grze z serwerem
        /// </summary>
        /// <value>The ID.</value>
        [XmlAttribute("ID")]
        public string ID
        {
            get;
            set;
        }

        [XmlArray("StartSlots")]
        [XmlArrayItem("Slot")]
        public Point[] StartSlots
        {
            get;
            set;
        }

        /// <summary>
        /// Zapisuje najlepsze wyniki do pliku
        /// </summary>
        public void SaveHighscores()
        {
            Highscores.Save(HighscoresFilePath);
        }

        /// <summary>
        /// Wczytuje definicje trasy z pliku
        /// </summary>
        /// <param name="uri">Pelna sciezka do pliku</param>
        /// <returns>Wczytana trasa</returns>
        public static Track Create(string uri)
        {
            //przeparsuj definicje trasy
            XmlReader reader = XmlReader.Create(uri);
            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(Track));
                return (Track)serializer.Deserialize(reader);
            }
            finally
            {
                reader.Close();
            }
        }

        /// <summary>
        /// Zapisuje definicje trasy do pliku. Dziala tylko jesli trasa zostala wczytana z pliku.
        /// </summary>
        /// <param name="track">Trasa do zapisania</param>
        public static void Save(Track track)
        {
            if (track.ContainingDirectory == null)
            {
                var dir = Directory.CreateDirectory(Environment.CurrentDirectory + "/Tracks/" + track.Title);
                track.ContainingDirectory = "Tracks/" + track.Title;
                Save(track, dir.FullName + "/track.xml");
            }
            else
                Save(track, Environment.CurrentDirectory + "/" + track.ContainingDirectory + "track.xml");
        }

        /// <summary>
        /// Zapisuje definicje trasy do pliku
        /// </summary>
        /// <param name="track">Trasa do zapisania</param>
        /// <param name="uri">Pelna sciezka do pliku</param>
        public static void Save(Track track, string uri)
        {
            XmlWriter writer = XmlWriter.Create(uri, new XmlWriterSettings()
            {
                Indent = true
            });
            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(Track));
                serializer.Serialize(writer, track);
            }
            finally
            {
                writer.Close();
            }
        }

        /// <summary>
        /// Sprawdza czy podany podana pozycja jest wolna (tj. pixel rozny od czarnego). Przyjmuje kordynaty XY gracza, ktore sa liczbami calkowitymi.
        /// </summary>
        /// <param name="x">X</param>
        /// <param name="y">Y</param>
        /// <returns></returns>
        public bool IsPositionFree(int x, int y)
        {
            return IsPixelFree((int)(x * Bitmap.PixelHeight / Height), (int)(y * Bitmap.PixelWidth / Width));
        }

        /// <summary>
        /// Sprawdza czy podany pixel jest wolny (tj. rozny od zera). Przyjmuje kordynaty XY ktore sa wspolrzednymi pixela w bitmapie.
        /// </summary>
        /// <param name="x">X</param>
        /// <param name="y">Y</param>
        /// <returns></returns>
        public bool IsPixelFree(int x, int y)
        {
            unsafe
            {
                int* buffer = (int*)Bitmap.BackBuffer;
                buffer += x;
                buffer += y * Bitmap.BackBufferStride / 4;

                int color = *buffer;

                byte red = (byte)(color >> 16);
                byte green = (byte)(color >> 8);
                byte blue = (byte)(color >> 0);

                if (red < 128 && green < 128 && blue < 128)
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Sprawdza czy podany pixel jest czarny czyli ,ze jest poza droga. Przyjmuje kordynaty XY ktore sa wspolrzednymi pixela w bitmapie.
        /// </summary>
        /// <param name="x">X</param>
        /// <param name="y">Y</param>
        /// <returns></returns>
        /// 
        public bool IsPixelBlack(int x, int y)
        {
            unsafe
            {
                int* buffer = (int*)Bitmap.BackBuffer;
                buffer += x;
                buffer += y * Bitmap.BackBufferStride / 4;

                int color = *buffer;

                byte red = (byte)(color >> 16);
                byte green = (byte)(color >> 8);
                byte blue = (byte)(color >> 0);

                if (red < 128 && green < 128 && blue < 128)
                    return true;
            }

            return false;
        }

        public bool IsPixelStart(int x, int y)
        {
            unsafe
            {
                int* buffer = (int*)Bitmap.BackBuffer;
                buffer += x;
                buffer += y * Bitmap.BackBufferStride / 4;

                int color = *buffer;

                byte red = (byte)(color >> 16);
                byte green = (byte)(color >> 8);
                byte blue = (byte)(color >> 0);

                if (red < 128 || green > 128 || blue > 128)
                    return false;
            }

            return true;
        }

        public bool IsPixelFinish(int x, int y)
        {
            unsafe
            {
                int* buffer = (int*)Bitmap.BackBuffer;
                buffer += x;
                buffer += y * Bitmap.BackBufferStride / 4;

                int color = *buffer;

                byte red = (byte)(color >> 16);
                byte green = (byte)(color >> 8);
                byte blue = (byte)(color >> 0);

                if (red > 128 || green < 128 || blue > 128)
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Zwraca punkt kolizji dla podanej sciezki.
        /// </summary>
        /// <param name="x1">The start x.</param>
        /// <param name="y1">The start y.</param>
        /// <param name="x2">The end x.</param>
        /// <param name="y2">The end y.</param>
        /// <returns>(-1,-1) jesli nie ma kolizji. Kordynaty okreslaja polozenie pixela.</returns>
        /// 
        public Point GetCollisionPoint(double x1, double y1, double x2, double y2)
        {

            int pEndX = (int)(((double)x2 / Width) * Bitmap.PixelWidth);
            int pEndY = (int)(((double)y2 / Height) * Bitmap.PixelHeight);
            return GetCollisionPoint(x1, y1, pEndX, pEndY);
        }

        public Point GetCollisionPoint(double x1, double y1, int pEndX, int pEndY)
        {
            int pStartX = (int)(((double)x1 / Width) * Bitmap.PixelWidth);
            int pStartY = (int)(((double)y1 / Height) * Bitmap.PixelHeight);

            int pDeltaX = Math.Abs(pEndX - pStartX);
            int pDeltaY = Math.Abs(pEndY - pStartY);

            int dx = pEndX > pStartX ? 1 : -1;
            int dy = pEndY > pStartY ? 1 : -1;

            int x = 0;
            int y = 0;

            //search for collision in oX
            while (x < pDeltaX)
            {
                if (!IsPixelFree((x + 1) * dx + pStartX, (int)((x + 1) * pDeltaY / pDeltaX) * dy + pStartY))
                    return new Point((x * dx + pStartX), (y * dy + pStartY));

                x++;
                y = x * pDeltaY / pDeltaX;
            }

            //search for collision in oY
            if (pDeltaX == 0)
            {
                while (y < pDeltaY)
                {
                    if (!IsPixelFree(x * dx + pStartX, (y + 1) * dy + pStartY))
                        return new Point((x * dx + pStartX), (y * dy + pStartY));
                    y++;
                }
            }

            return new Point(-1, -1);
        }

        public Point GetFinishCrossPoint(double x1, double y1, double x2, double y2)
        {
            int pStartX = (int)(((double)x1 / Width) * Bitmap.PixelWidth);
            int pEndX = (int)(((double)x2 / Width) * Bitmap.PixelWidth);
            int pStartY = (int)(((double)y1 / Height) * Bitmap.PixelHeight);
            int pEndY = (int)(((double)y2 / Height) * Bitmap.PixelHeight);

            int pDeltaX = Math.Abs(pEndX - pStartX);
            int pDeltaY = Math.Abs(pEndY - pStartY);

            int dx = pEndX > pStartX ? 1 : -1;
            int dy = pEndY > pStartY ? 1 : -1;

            int x = 0;
            int y = 0;
            int checkX, checkY;

            Point? finish = null;
            Point? start = null;

            //search for collision in oX
            while (x < pDeltaX)
            {
                checkX = (x + 1) * dx + pStartX;
                checkY = (int)((x + 1) * pDeltaY / pDeltaX) * dy + pStartY;

                x++;
                y = x * pDeltaY / pDeltaX;

                if (IsPixelBlack(checkX, checkY))
                    return new Point(-1, -1);

                //nie znaleziono jeszcze poczatku mety
                if (finish == null)
                {
                    //mamy poczatek mety
                    if (IsPixelFinish(checkX, checkY))
                        finish = new Point(checkX, checkY);
                }
                else //mamy poczatek mety
                {
                    if (IsPixelStart(checkX, checkY))
                    {
                        //mamy koniec mety
                        start = new Point(checkX, checkY);
                        return start.Value;
                    }
                    else if (!IsPixelFinish(checkX, checkY))
                        break; //to nie meta albo zla maska
                }
            }

            //search for collision in oY
            if (pDeltaX == 0)
            {
                while (y < pDeltaY)
                {
                    checkX = x * dx + pStartX;
                    checkY = (y + 1) * dy + pStartY;

                    y++;

                    if (IsPixelBlack(checkX, checkY))
                        return new Point(-1, -1);

                    //nie znaleziono jeszcze poczatku mety
                    if (finish == null)
                    {
                        //mamy poczatek mety
                        if (IsPixelFinish(checkX, checkY))
                            finish = new Point(checkX, checkY);
                    }
                    else //mamy poczatek mety
                    {
                        if (IsPixelStart(checkX, checkY))
                        {
                            //mamy koniec mety
                            start = new Point(checkX, checkY);
                            return start.Value;
                        }
                        else if (!IsPixelFinish(checkX, checkY))
                            break; //to nie meta albo zla maska
                    }
                }
            }

            return new Point(-1, -1);
        }

        public Point GetStartCrossPoint(double x1, double y1, double x2, double y2)
        {
            int pStartX = (int)(((double)x1 / Width) * Bitmap.PixelWidth);
            int pEndX = (int)(((double)x2 / Width) * Bitmap.PixelWidth);
            int pStartY = (int)(((double)y1 / Height) * Bitmap.PixelHeight);
            int pEndY = (int)(((double)y2 / Height) * Bitmap.PixelHeight);

            int pDeltaX = Math.Abs(pEndX - pStartX);
            int pDeltaY = Math.Abs(pEndY - pStartY);

            int dx = pEndX > pStartX ? 1 : -1;
            int dy = pEndY > pStartY ? 1 : -1;

            int x = 0;
            int y = 0;
            int checkX, checkY;

            Point? finish = null;
            Point? start = null;

            //search for collision in oX
            while (x < pDeltaX)
            {
                checkX = (x + 1) * dx + pStartX;
                checkY = (int)((x + 1) * pDeltaY / pDeltaX) * dy + pStartY;

                x++;
                y = x * pDeltaY / pDeltaX;
                if(IsPixelBlack(checkX, checkY))
                    return new Point(-1, -1);
                //nie znaleziono jeszcze poczatku mety
                if (finish == null)
                {
                    //mamy poczatek mety
                    if (IsPixelStart(checkX, checkY))
                        finish = new Point(checkX, checkY);
                }
                else //mamy poczatek mety
                {
                    if (IsPixelFinish(checkX, checkY))
                    {
                        //mamy koniec mety
                        start = new Point(checkX, checkY);
                        return start.Value;
                    }
                    else if (!IsPixelStart(checkX, checkY))
                        break; //to nie meta albo zla maska
                }
            }

            //search for collision in oY
            if (pDeltaX == 0)
            {
                while (y < pDeltaY)
                {
                    checkX = x * dx + pStartX;
                    checkY = (y + 1) * dy + pStartY;

                    y++;

                    if (IsPixelBlack(checkX, checkY))
                        return new Point(-1, -1);

                    //nie znaleziono jeszcze poczatku mety
                    if (finish == null)
                    {
                        //mamy poczatek mety
                        if (IsPixelStart(checkX, checkY))
                            finish = new Point(checkX, checkY);
                    }
                    else //mamy poczatek mety
                    {
                        if (IsPixelFinish(checkX, checkY))
                        {
                            //mamy koniec mety
                            start = new Point(checkX, checkY);
                            return start.Value;
                        }
                        else if (!IsPixelStart(checkX, checkY))
                            break; //to nie meta albo zla maska
                    }
                }
            }

            return new Point(-1, -1);
        }

        #region ICloneable Members

        public object Clone()
        {
            Track clone = (Track)this.MemberwiseClone();
            clone._mask = null;
            clone._background = null;
            return clone;
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            try
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
            catch (Exception) { }
        }

        #endregion

    }
}
